package mspbots.xero.client.core.client;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.log4j.Log4j2;
import mspbots.xero.client.oauth.AuthService;
import mspbots.xero.client.oauth.TokenState;
import mspbots.xero.common.QueryParams;
import mspbots.xero.common.RabbitKeys;
import mspbots.xero.common.ResultSync;
import mspbots.xero.common.annotation.ApiException;
import org.springframework.amqp.rabbit.core.RabbitMessagingTemplate;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriBuilder;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * com.mspbots.sync.wise.client.AutoPagerClient
 *
 * @author Alex bob(https://github.com/vnobo)
 * @date Created by 2020/7/3
 */
@Log4j2
@Service
public class XeroClient {

    private final WebClient webClient;
    private final ObjectMapper objectMapper;
    private final RabbitMessagingTemplate rabbitMessagingTemplate;

    private final AuthService authService;

    public XeroClient(WebClient.Builder builder,
                      RabbitMessagingTemplate rabbitMessagingTemplate,
                      ObjectMapper objectMapper, AuthService authService) {

        this.rabbitMessagingTemplate = rabbitMessagingTemplate;
        this.objectMapper = objectMapper;
        this.authService = authService;

        this.webClient = builder.baseUrl("https://api.xero.com")
                .defaultHeaders(httpHeaders -> httpHeaders.setContentType(MediaType.APPLICATION_JSON))
                .defaultHeaders(httpHeaders -> httpHeaders.setAccept(List.of(MediaType.APPLICATION_JSON)))
                .build();
    }

    public Flux<ResultData> get(String path, QueryParams queryParams) {
        return this.get(path, queryParams, Map.of());
    }

    public Flux<ResultData> get(String path, QueryParams queryParams, Map<String, Object> pathParams) {
        return Mono.deferContextual(ctx -> this.authService.getToken(ctx.get("tokenState")))
                .flatMapMany(tokenState -> this.getHelp(tokenState, uriBuilder -> uriBuilder.path(path)
                        .queryParams(queryParams).build(pathParams)));
    }

    private Flux<ResultData> getHelp(TokenState tokenState, Function<UriBuilder, URI> uriFunction) {
        List<Mono<ResultData>> gets = tokenState.getXeroTenants().findValues("tenantId")
                .parallelStream()
                .map(JsonNode::textValue)
                .map(xeroTenantId -> this.getResultSync(tokenState,xeroTenantId, uriFunction)
                        .flatMap(resultSync -> this.webClient.get().uri(uriFunction)
                                .headers(httpHeaders -> httpHeaders.setBearerAuth(tokenState.getAccessToken()))
                                .header("Xero-tenant-id", xeroTenantId)
                                .exchangeToMono(clientResponse ->
                                        this.handlerRequestResult(clientResponse, resultSync))))
                .collect(Collectors.toList());
        return Flux.concat(gets);

    }

    private Mono<ResultSync> getResultSync(TokenState tokenState,String xeroTenantId, Function<UriBuilder, URI> uriFunction) {
        URI uri = uriFunction.apply(UriComponentsBuilder.newInstance());
        return Mono.deferContextual(ctx -> {
            ResultSync resultSync = ctx.getOrDefault("resultSync",
                    ResultSync.withDefault(uri.getRawPath()));
            assert resultSync != null;
            resultSync.setTenantId(tokenState.getTenantId());
            resultSync.setXeroTenantId(xeroTenantId);
            resultSync.setParams(this.objectMapper
                    .convertValue(UriComponentsBuilder.fromUri(uri).build().getQueryParams(), JsonNode.class));
            resultSync.setElapsedTime(System.currentTimeMillis());
            return Mono.just(resultSync);
        });
    }

    private Mono<ResultData> handlerRequestResult(ClientResponse response, ResultSync resultSync) {
        resultSync.setStatus(response.statusCode());
        resultSync.setElapsedTime(System.currentTimeMillis() - resultSync.getElapsedTime());

        if (response.statusCode().isError()) {
            var bodyMono = response.headers().contentType().stream()
                    .allMatch(MediaType.APPLICATION_JSON::isCompatibleWith) ? response.bodyToMono(JsonNode.class)
                    .defaultIfEmpty(this.objectMapper.createObjectNode()
                            .put("error", "This request API error.")
                            .putPOJO("headers", response.headers().asHttpHeaders())) :
                    response.bodyToMono(String.class);
            return bodyMono.doOnNext(errNode -> {
                resultSync.setMsg(errNode);
                log.debug("sync [{}] data [{}] error action {}, elapsedTime {}, status: {}",
                        resultSync.getTenantId(), resultSync.getXeroTenantId(),resultSync.getAction(),
                        resultSync.getElapsedTime(), resultSync.getStatus());

                this.rabbitMessagingTemplate.convertAndSend(RabbitKeys.BASE_EXCHANGE_NAME,
                        RabbitKeys.LOGS_TASK_QUEUE_KEY, resultSync);
            }).flatMap(errNode -> Mono.error(ApiException.withMsg(response.statusCode(), errNode)));

        }

        long contentLength = response.headers().contentLength().orElse(0);
        int count = contentLength < 1024 ? 1 : (int) contentLength / 1024;
        resultSync.setCount(count);

        log.debug("Sync [{}] data  [{}] success, action [{}], elapsedTime [{}], count [{}]",
                resultSync.getTenantId(),resultSync.getXeroTenantId(), resultSync.getAction(),
                resultSync.getElapsedTime(), resultSync.getCount());

        this.rabbitMessagingTemplate.convertAndSend(RabbitKeys.BASE_EXCHANGE_NAME,
                RabbitKeys.LOGS_TASK_QUEUE_KEY, resultSync);

        return response.bodyToMono(JsonNode.class)
                .map(entity -> ResultData.builder()
                        .xeroTenantId(resultSync.getXeroTenantId())
                        .tenantId(resultSync.getTenantId())
                        .entity(entity).build());
    }

}
